const path = require("path");
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const types = require("@babel/types");
const generator = require("@babel/generator").default;
const { getSource } = require("./utils");

//将\替换成/
function toUnixPath(filePath) {
  return filePath.replace(/\\/g, "/");
}
//获取文件路径
function tryExtensions(modulePath, extensions) {
  if (fs.existsSync(modulePath)) {
    return modulePath;
  }
  for (let i = 0; i < extensions?.length; i++) {
    let filePath = modulePath + extensions[i];
    if (fs.existsSync(filePath)) {
      return filePath;
    }
  }
  throw new Error(`无法找到${modulePath}`);
}
const baseDir = toUnixPath(process.cwd());

class Compilation {
  constructor(webpackOptions) {
    this.options = webpackOptions;
    this.modules = []; //本次编译所有生成出来的模块
    this.chunks = []; //本次编译产出的所有代码块，入口模块和依赖的模块打包在一起为代码块
    this.assets = {}; //本次编译产出的资源文件
    this.fileDependencies = []; //本次打包涉及到的文件，这里主要是为了实现watch模式下监听文件的变化，文件发生变化后会重新编译
  }

  //开始编译
  build(onCompiled) {
    //第五步：根据配置文件中的`entry`配置项找到所有的入口
    let entry = {};
    if (typeof this.options.entry === "string") {
      entry.main = this.options.entry; //如果是单入口，将entry:"xx"变成{main:"xx"}，这里需要做兼容
    } else {
      entry = this.options.entry;
    }
    // 第六步：从入口模块开始编译
    // 6.1 把入口文件的绝对路径添加到依赖数组（this.fileDependencies）中，记录此次编译依赖的模块
    for (let entryName in entry) {
      //path.posix为了解决不同操作系统的路径分隔符,这里拿到的就是入口文件的绝对路径
      const entryFilePath = path.posix.join(baseDir, entry[entryName]);
      this.fileDependencies.push(entryFilePath);
      // 6.2 得到入口模块的的 module 对象 （里面放着该模块的路径、依赖模块、源代码等）
      // buildModule 的时候会直接把模块放到 this.modules 中
      const entryModule = this.buildModule(entryName, entryFilePath);

      //第八步：等所有模块都编译完成后，根据模块之间的依赖关系，组装代码块 `chunk`（一般来说，每个入口文件会对应一个代码块`chunk`，每个代码块`chunk`里面会放着本入口模块和它依赖的模块）
      const chunk = {
        name: entryName,
        entryModule, //此代码块对应的module的对象
        modules: this.modules.filter((item) => item.names.includes(entryName)), //找出属于该代码块的模块
      };
      this.chunks.push(chunk);
    }

    //第九步：生成运行时代码
    this.chunks.forEach((chunk) => {
      const filename = this.options.output.filename.replace("[name]", chunk.name);
      const code = getSource(chunk);
      this.assets[filename] = code;
    })

    // 编译完成后执行回调
    onCompiled(null, {
      assets: this.assets,
      modules: this.modules,
      chunks: this.chunks
    }, this.fileDependencies);
  }

  //编译模块
  buildModule(name, modulePath) {
    //6.2.1 读取模块内容，获取源代码
    let sourceCode = fs.readFileSync(modulePath, "utf8");
    // buildModule最终会返回一个modules模块对象，每个模块都会有一个id,id是相对于根目录的相对路径
    const moduleId = "./" + path.posix.relative(baseDir, modulePath);
    //6.2.2 创建模块对象
    const module = {
      id: moduleId,
      names: [name], //names设计成数组是因为代表的是此模块属于哪个代码块，可能属于多个代码块
      dependencies: [], //它依赖的模块
      _source: "", //该模块的代码信息
    };

    // 判断一下之前是否有这个模块，没有的话先推进去 后续信息再补充进去
    // 防止循环引用 堆栈溢出
    const existModule = this.modules.find((item) => item.id === moduleId);
    if (!existModule) {
      this.modules.push(module);
    }

    const loaders = [];
    const { rules } = this.options.module;
    //6.2.3 找到对应的 Loader 对源代码进行翻译和替换
    rules.forEach((rule) => {
      const { test, use } = rule;

      if (test.test(modulePath)) {
        loaders.push(...use);
      }
    });

    //6.2.4 从后往前执行loader
    sourceCode = loaders.reduceRight((source, loader) => {
      return loader(source);
    }, sourceCode);

    //通过loader翻译后的内容一定得是js内容，因为最后得走我们babel-parse，只有js才能成编译AST
    //第七步：找出此模块所依赖的模块，再对依赖模块进行编译
    //7.1：先把源代码编译成 [AST](https://astexplorer.net/)
    let ast = parser.parse(sourceCode, { sourceType: "module" });

    // 7.2：遍历 AST，找到所有的 require 语句 记录依赖 并修改成绝对路径
    traverse(ast, {
      CallExpression: ({ node }) => {
        if (node.callee.name === "require") {
          const depModuleName = node.arguments[0].value; //获取依赖的模块
          const dirname = path.posix.dirname(modulePath); //获取当前正在编译的模所在的目录
          let depModulePath = path.posix.join(dirname, depModuleName); //获取依赖模块的绝对路径
          const extensions = this.options.resolve?.extensions || [".js"]; //获取配置中的extensions
          depModulePath = tryExtensions(depModulePath, extensions); //尝试添加后缀，找到一个真实在硬盘上存在的文件

          // 7.3：将依赖模块的绝对路径 push 到 `this.fileDependencies` 中
          this.fileDependencies.push(depModulePath); //把依赖模块的绝对路径添加到依赖数组中
          //7.4：生成依赖模块的`模块 id`
          const depModuleId =
            "./" + path.posix.relative(baseDir, depModulePath);
          //7.5：修改语法结构，把依赖的模块改为依赖`模块 id`(绝对路径)
          node.arguments = [types.stringLiteral(depModuleId)];
          //7.6：将依赖模块的信息 push 到该模块的 `dependencies` 属性中
          module.dependencies.push({ depModuleId, depModulePath });
        }
      },
    });

    //7.7：生成新代码，并把转译后的源代码放到 `module._source` 属性上
    const { code } = generator(ast);
    module._source = code;

    // 7.8：递归编译依赖模块
    module.dependencies.forEach(({ depModuleId, depModulePath }) => {
      //考虑到多入口打包 ：一个模块被多个其他模块引用，不需要重复打包
      const existModule = this.modules.find((item) => item.id === depModuleId);
      if (existModule) {
        //names指的是它属于哪个代码块chunk
        // 记录一下 他属于哪些代码块
        existModule.names.push(name);
      } else {
        // 递归编译
        this.buildModule(name, depModulePath);
      }
    });
    return module;
  }
}

module.exports = Compilation;
